/*
 * Copyright 2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.oschina.durcframework.easymybatis.ext;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.QName;
import org.dom4j.dom.DOMAttribute;
import org.dom4j.io.SAXReader;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;

import net.oschina.durcframework.easymybatis.dao.Dao;
import net.oschina.durcframework.easymybatis.ext.code.client.ClassClient;

/**
 * mapper构建
 * @author tanghc
 *
 */
public class MapperLocationsBuilder {

	private static Log LOGGER = LogFactory.getLog(MapperLocationsBuilder.class);
	
	private static final String XML_SUFFIX = ".xml";
	private static final String MAPPER_START = "<mapper>";
	private static final String MAPPER_END = "</mapper>";
	
	private static final String TEMPLATE_SUFFIX = ".vm";
	
	private static final String COMMON_SQL_CLASSPATH = "easymybatis/commonSql.xml";
	
	private static String DEFAULT_CLASS_PATH = "/easymybatis/tpl/";
	
	private static ClassClient codeClient = new ClassClient();
	
	private String commonSqlClasspath = COMMON_SQL_CLASSPATH;
	
	private Map<String, MapperResourceDefinition> mapperResourceStore = new HashMap<>();
	
	private SAXReader saxReader;
	
	private Attribute NAMESPACE;
	
	private DataSource dataSource;
	
	private String templateClasspath;
	
	
	/**
	 * 初始化mybatis配置文件
	 * 
	 * @param basePackage
	 *            实体类的包目录.指定哪些包需要被扫描,支持多个包"package.a,package.b"并对每个包都会递归搜索
	 */
	public Resource[] build(String basePackage) {
		init();
		try {
			String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
					ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
			ClassScanner classScanner = new ClassScanner(basePackages, Dao.class);
			Set<Class<?>> clazzsSet = classScanner.getClassSet();

			Resource[] mapperLocations = this.buildMapperLocations(clazzsSet);

			return mapperLocations;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}finally {
			distroy();
		}
	}
	
	private void init() {
		saxReader = new SAXReader();
		NAMESPACE = new DOMAttribute(new QName("namespace"));
	}
	
	private void distroy() {
		saxReader = null;
		NAMESPACE = null;
		mapperResourceStore.clear();
	}

	
	// 存储xml文件
	public void storeMapperFile(Resource[] mapperLocations) {
		for (Resource mapperLocation : mapperLocations) {
			String filename = mapperLocation.getFilename();// XxDao.xml
			
			mapperResourceStore.put(filename, new MapperResourceDefinition(mapperLocation));
		}
	}
	
	private MapperResourceDefinition getMapperFile(String mapperFileName) {
		return mapperResourceStore.get(mapperFileName);
	}

	private Resource[] buildMapperLocations(Set<Class<?>> clazzsSet) {
		
		List<Resource> mapperLocations = new ArrayList<>(clazzsSet.size());
		String templateClasspath = this.buildTemplateClasspath(this.getDataSource());
		
		for (Class<?> daoClass : clazzsSet) {

			String xml = codeClient.genMybatisXml(daoClass, templateClasspath);

			xml = this.modifyFileContent(daoClass, xml);
			
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("生成mybatis文件:\r\n" + xml);
			}
			
			 /*	必须指明InputStreamResource的description
			  	主要解决多个dao报Invalid bound statement (not found)BUG。
			  	经排查是由XMLMapperBuilder.parse()中，configuration.isResourceLoaded(resource)返回true导致。
			  	因为InputStreamResource.toString()始终返回同一个值，需要指定description保证InputStreamResource.toString()唯一。
			  */
			Resource resource = new InputStreamResource(new ByteArrayInputStream(xml.getBytes()), daoClass.getName());
			mapperLocations.add(resource);
		}
		
		this.addUnmergedResource(mapperLocations);
		
		this.addCommonSqlClasspathMapper(mapperLocations);

		return mapperLocations.toArray(new Resource[mapperLocations.size()]);
	}
	
	
	private String buildTemplateClasspath(DataSource dataSource) {
		if(this.templateClasspath != null) {
			return this.templateClasspath;
		}
		String dbName = this.buildDbName(dataSource);
		// 返回格式：classpath路径 + 数据库名称 + 文件后缀
		// 如：/easymybatis/tpl/mysql.vm
		return DEFAULT_CLASS_PATH + this.buildTemplateFileName(dbName);
	}
	
	// 构建文件名
	private String buildTemplateFileName(String dbName) {
		dbName = dbName.replaceAll("\\s", "").toLowerCase();
		return dbName + TEMPLATE_SUFFIX;
	}
	
	// 获取数据库类型
	private String buildDbName(DataSource dataSource) {
		if(dataSource == null) {
			throw new NullPointerException("dataSource 不能为null");
		}
		Connection conn = null;
		try {
			conn = dataSource.getConnection();
			
			DatabaseMetaData metaData = conn.getMetaData();
			
			String dbName = metaData.getDatabaseProductName();
			
			LOGGER.info("数据库名称：" + dbName);
			
			return dbName;
			
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
			throw new RuntimeException("获取数据库信息失败，是否正确设置了DataSource。");
		}finally {
			if(conn != null) {
				try {
					conn.close();
				} catch (SQLException e) {
					LOGGER.error(e.getMessage(), e);
				}
			}
		}
	}
	
	private void addCommonSqlClasspathMapper(List<Resource> mapperLocations) {
		ClassPathResource res = new ClassPathResource(commonSqlClasspath);
		mapperLocations.add(res);
	}

	private void addUnmergedResource(List<Resource> mapperLocations) {
		Collection<MapperResourceDefinition> mapperResourceDefinitions = this.mapperResourceStore.values();
		for (MapperResourceDefinition mapperResourceDefinition : mapperResourceDefinitions) {
			if(mapperResourceDefinition.isMerged()) {
				continue;
			}
			
			LOGGER.info("加载未合并Mapper：" + mapperResourceDefinition.getFilename());
			
			mapperLocations.add(mapperResourceDefinition.getResource());
		}
	}
	
	// 修改文件内容
	private String modifyFileContent(Class<?> mapperClass, String xml) {
		// 自定义文件
		String mapperFileName = mapperClass.getSimpleName() + XML_SUFFIX;
		MapperResourceDefinition mapperResourceDefinition = this.getMapperFile(mapperFileName);

		if (mapperResourceDefinition == null) {
			return xml;
		}
		StringBuilder sb = new StringBuilder();

		sb.append(xml.replace(MAPPER_END, "")); // 先去掉</mapper>
		// 追加内容
		String extFileContent = this.getExtFileContent(mapperResourceDefinition.getResource());
		
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("追加自定义sql:\r\n " + extFileContent);
		}
		sb.append(extFileContent).append(MAPPER_END); // 追加自定义sql,加上</mapper>
		
		mapperResourceDefinition.setMerged(true);

		return sb.toString();

	}

	private String getExtFileContent(Resource mapperLocation) {
		try {
			InputStream in = mapperLocation.getInputStream();
		    Document document = saxReader.read(in); 
		    Element mapperNode = document.getRootElement();
		    
		    String rootNodeName = mapperNode.getName();
		    
		    if(!"mapper".equals(rootNodeName)) {
		    	throw new Exception("mapper文件必须含有<mapper>节点,是否缺少<mapper></mapper>?");
		    }
		    
		    mapperNode.remove(NAMESPACE);
		    
		    String rootXml = mapperNode.asXML();
		    // 去除首尾mapper标签
		    rootXml = rootXml.replace(MAPPER_START, "").replace(MAPPER_END, "");
		    
			return rootXml;
		} catch (Exception e) {
			LOGGER.error(e.getMessage(),e);
			throw new RuntimeException("加载资源文件出错," + e.getMessage());
		} 
	}
	
	
	public DataSource getDataSource() {
		return dataSource;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setCommonSqlClasspath(String commonSqlClasspath) {
		this.commonSqlClasspath = commonSqlClasspath;
	}

	public String getTemplateClasspath() {
		return templateClasspath;
	}

	public void setTemplateClasspath(String templateClasspath) {
		this.templateClasspath = templateClasspath;
	}
	
	// MapperResource包装类
	private static class MapperResourceDefinition {
		// 是否合并
		private boolean merged;
		// mapper对应的resource
		private Resource resource;

		public MapperResourceDefinition(Resource resource) {
			super();
			this.resource = resource;
		}
		
		public String getFilename() {
			return this.resource.getFilename();
		}

		public boolean isMerged() {
			return merged;
		}

		public void setMerged(boolean merged) {
			this.merged = merged;
		}

		public Resource getResource() {
			return resource;
		}
	}


}
